// Copyright 2003-2005 Arthur van Hoff, Rick Blair
// Licensed under Apache License version 2.0
// Original license LGPL

package javax.jmdns.impl;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.jmdns.NetworkTopologyDiscovery;
import javax.jmdns.impl.constants.DNSRecordClass;
import javax.jmdns.impl.constants.DNSRecordType;
import javax.jmdns.impl.constants.DNSState;
import javax.jmdns.impl.tasks.DNSTask;

/**
 * HostInfo information on the local host to be able to cope with change of addresses.
 * @author Pierre Frisch, Werner Randelshofer
 */
public class HostInfo implements DNSStatefulObject {
   private static Logger logger = Logger.getLogger(HostInfo.class.getName());

   protected String _name;

   protected InetAddress _address;

   protected NetworkInterface _interfaze;

   private final HostInfoState _state;

   private final static class HostInfoState extends DNSStatefulObject.DefaultImplementation {

      private static final long serialVersionUID = -8191476803620402088L;

      /**
       * @param dns
       */
      public HostInfoState(JmDNSImpl dns) {
         super();
         this.setDns(dns);
      }

   }

   /**
    * @param address IP address to bind
    * @param dns JmDNS instance
    * @param jmdnsName JmDNS name
    * @return new HostInfo
    */
   public static HostInfo newHostInfo(InetAddress address, JmDNSImpl dns, String jmdnsName) {
      HostInfo localhost = null;
      String aName = "";
      InetAddress addr = address;
      try {
         if (addr == null) {
            String ip = System.getProperty("net.mdns.interface");
            if (ip != null) {
               addr = InetAddress.getByName(ip);
            } else {
               addr = InetAddress.getLocalHost();
               if (addr.isLoopbackAddress()) {
                  // Find local address that isn't a loopback address
                  InetAddress[] addresses = NetworkTopologyDiscovery.Factory.getInstance().getInetAddresses();
                  if (addresses.length > 0) {
                     addr = addresses[0];
                  }
               }
            }
            aName = addr.getHostName();
            if (addr.isLoopbackAddress()) {
               logger.warning("Could not find any address beside the loopback.");
            }
         } else {
            aName = addr.getHostName();
         }
         if (aName.contains("in-addr.arpa") || (aName.equals(addr.getHostAddress()))) {
            aName = ((jmdnsName != null) && (jmdnsName.length() > 0) ? jmdnsName : addr.getHostAddress());
         }
      } catch (final IOException e) {
         logger.log(Level.WARNING, "Could not intialize the host network interface on " + address + "because of an error: " + e.getMessage(), e);
         // This is only used for running unit test on Debian / Ubuntu
         addr = loopbackAddress();
         aName = ((jmdnsName != null) && (jmdnsName.length() > 0) ? jmdnsName : "computer");
      }
      // A host name with "." is illegal. so strip off everything and append .local.
      aName = aName.replace('.', '-');
      aName += ".local.";
      localhost = new HostInfo(addr, aName, dns);
      return localhost;
   }

   private static InetAddress loopbackAddress() {
      try {
         return InetAddress.getByName(null);
      } catch (UnknownHostException exception) {
         return null;
      }
   }

   /**
    * This is used to create a unique name for the host name.
    */
   private int hostNameCount;

   private HostInfo(final InetAddress address, final String name, final JmDNSImpl dns) {
      super();
      this._state = new HostInfoState(dns);
      this._address = address;
      this._name = name;
      if (address != null) {
         try {
            _interfaze = NetworkInterface.getByInetAddress(address);
         } catch (Exception exception) {
            logger.log(Level.SEVERE, "LocalHostInfo() exception ", exception);
         }
      }
   }

   public String getName() {
      return _name;
   }

   public InetAddress getInetAddress() {
      return _address;
   }

   Inet4Address getInet4Address() {
      if (this.getInetAddress() instanceof Inet4Address) {
         return (Inet4Address) _address;
      }
      return null;
   }

   Inet6Address getInet6Address() {
      if (this.getInetAddress() instanceof Inet6Address) {
         return (Inet6Address) _address;
      }
      return null;
   }

   public NetworkInterface getInterface() {
      return _interfaze;
   }

   synchronized String incrementHostName() {
      hostNameCount++;
      int plocal = _name.indexOf(".local.");
      int punder = _name.lastIndexOf('-');
      _name = _name.substring(0, (punder == -1 ? plocal : punder)) + "-" + hostNameCount + ".local.";
      return _name;
   }

   boolean shouldIgnorePacket(DatagramPacket packet) {
      boolean result = false;
      if (this.getInetAddress() != null) {
         InetAddress from = packet.getAddress();
         if (from != null) {
            if (from.isLinkLocalAddress() && (!this.getInetAddress().isLinkLocalAddress())) {
               // Ignore linklocal packets on regular interfaces, unless this is
               // also a linklocal interface. This is to avoid duplicates. This is
               // a terrible hack caused by the lack of an API to get the address
               // of the interface on which the packet was received.
               result = true;
            }
            if (from.isLoopbackAddress() && (!this.getInetAddress().isLoopbackAddress())) {
               // Ignore loopback packets on a regular interface unless this is also a loopback interface.
               result = true;
            }
         }
      }
      return result;
   }

   DNSRecord.Address getDNSAddressRecord(DNSRecordType type, boolean unique, int ttl) {
      switch (type) {
      case TYPE_A:
         return this.getDNS4AddressRecord(unique, ttl);
      case TYPE_A6:
      case TYPE_AAAA:
         return this.getDNS6AddressRecord(unique, ttl);
      default:
      }
      return null;
   }

   private DNSRecord.Address getDNS4AddressRecord(boolean unique, int ttl) {
      if ((this.getInetAddress() instanceof Inet4Address)
               || ((this.getInetAddress() instanceof Inet6Address) && (((Inet6Address) this.getInetAddress()).isIPv4CompatibleAddress()))) {
         return new DNSRecord.IPv4Address(this.getName(), DNSRecordClass.CLASS_IN, unique, ttl, this.getInetAddress());
      }
      return null;
   }

   private DNSRecord.Address getDNS6AddressRecord(boolean unique, int ttl) {
      if (this.getInetAddress() instanceof Inet6Address) {
         return new DNSRecord.IPv6Address(this.getName(), DNSRecordClass.CLASS_IN, unique, ttl, this.getInetAddress());
      }
      return null;
   }

   DNSRecord.Pointer getDNSReverseAddressRecord(DNSRecordType type, boolean unique, int ttl) {
      switch (type) {
      case TYPE_A:
         return this.getDNS4ReverseAddressRecord(unique, ttl);
      case TYPE_A6:
      case TYPE_AAAA:
         return this.getDNS6ReverseAddressRecord(unique, ttl);
      default:
      }
      return null;
   }

   private DNSRecord.Pointer getDNS4ReverseAddressRecord(boolean unique, int ttl) {
      if (this.getInetAddress() instanceof Inet4Address) {
         return new DNSRecord.Pointer(this.getInetAddress().getHostAddress() + ".in-addr.arpa.", DNSRecordClass.CLASS_IN, unique, ttl, this.getName());
      }
      if ((this.getInetAddress() instanceof Inet6Address) && (((Inet6Address) this.getInetAddress()).isIPv4CompatibleAddress())) {
         byte[] rawAddress = this.getInetAddress().getAddress();
         String address = (rawAddress[12] & 0xff) + "." + (rawAddress[13] & 0xff) + "." + (rawAddress[14] & 0xff) + "." + (rawAddress[15] & 0xff);
         return new DNSRecord.Pointer(address + ".in-addr.arpa.", DNSRecordClass.CLASS_IN, unique, ttl, this.getName());
      }
      return null;
   }

   private DNSRecord.Pointer getDNS6ReverseAddressRecord(boolean unique, int ttl) {
      if (this.getInetAddress() instanceof Inet6Address) {
         return new DNSRecord.Pointer(this.getInetAddress().getHostAddress() + ".ip6.arpa.", DNSRecordClass.CLASS_IN, unique, ttl, this.getName());
      }
      return null;
   }

   @Override
   public String toString() {
      StringBuilder buf = new StringBuilder(1024);
      buf.append("local host info[");
      buf.append(getName() != null ? getName() : "no name");
      buf.append(", ");
      buf.append(getInterface() != null ? getInterface().getDisplayName() : "???");
      buf.append(":");
      buf.append(getInetAddress() != null ? getInetAddress().getHostAddress() : "no address");
      buf.append(", ");
      buf.append(_state);
      buf.append("]");
      return buf.toString();
   }

   public Collection<DNSRecord> answers(boolean unique, int ttl) {
      List<DNSRecord> list = new ArrayList<DNSRecord>();
      DNSRecord answer = this.getDNS4AddressRecord(unique, ttl);
      if (answer != null) {
         list.add(answer);
      }
      answer = this.getDNS6AddressRecord(unique, ttl);
      if (answer != null) {
         list.add(answer);
      }
      return list;
   }

   /**
    * {@inheritDoc}
    */

   public JmDNSImpl getDns() {
      return this._state.getDns();
   }

   /**
    * {@inheritDoc}
    */

   public boolean advanceState(DNSTask task) {
      return this._state.advanceState(task);
   }

   /**
    * {@inheritDoc}
    */

   public void removeAssociationWithTask(DNSTask task) {
      this._state.removeAssociationWithTask(task);
   }

   /**
    * {@inheritDoc}
    */

   public boolean revertState() {
      return this._state.revertState();
   }

   /**
    * {@inheritDoc}
    */

   public void associateWithTask(DNSTask task, DNSState state) {
      this._state.associateWithTask(task, state);
   }

   /**
    * {@inheritDoc}
    */

   public boolean isAssociatedWithTask(DNSTask task, DNSState state) {
      return this._state.isAssociatedWithTask(task, state);
   }

   /**
    * {@inheritDoc}
    */

   public boolean cancelState() {
      return this._state.cancelState();
   }

   /**
    * {@inheritDoc}
    */

   public boolean closeState() {
      return this._state.closeState();
   }

   /**
    * {@inheritDoc}
    */

   public boolean recoverState() {
      return this._state.recoverState();
   }

   /**
    * {@inheritDoc}
    */

   public boolean isProbing() {
      return this._state.isProbing();
   }

   /**
    * {@inheritDoc}
    */

   public boolean isAnnouncing() {
      return this._state.isAnnouncing();
   }

   /**
    * {@inheritDoc}
    */

   public boolean isAnnounced() {
      return this._state.isAnnounced();
   }

   /**
    * {@inheritDoc}
    */

   public boolean isCanceling() {
      return this._state.isCanceling();
   }

   /**
    * {@inheritDoc}
    */

   public boolean isCanceled() {
      return this._state.isCanceled();
   }

   /**
    * {@inheritDoc}
    */

   public boolean isClosing() {
      return this._state.isClosing();
   }

   /**
    * {@inheritDoc}
    */

   public boolean isClosed() {
      return this._state.isClosed();
   }

   /**
    * {@inheritDoc}
    */

   public boolean waitForAnnounced(long timeout) {
      return _state.waitForAnnounced(timeout);
   }

   /**
    * {@inheritDoc}
    */

   public boolean waitForCanceled(long timeout) {
      if (_address == null) {
         // No need to wait this was never announced.
         return true;
      }
      return _state.waitForCanceled(timeout);
   }

}
